TypeScript 50.6%
JavaScript 46.1%
SQL 2.1%
Shell 0.6%
CSS 0.5%
1import { NextRequest, NextResponse } from "next/server";2import { and, eq, sql } from "drizzle-orm";3import { db } from "@/db/client";4import { listingMedia } from "@/db/schema";5import { assertSameOrigin, requireUser } from "@/lib/auth/session";6import { getListingFull, assertOwner } from "@/lib/listings/service";7import { processUpload, MediaError, MAX_BYTES } from "@/lib/listings/media";8import { apiError, handle } from "@/lib/api";9import { audit } from "@/lib/auth/audit";10export const dynamic = "force-dynamic";11export async function PUT(req: NextRequest, { params }: { params: Promise<{ token: string }> }) {12 try {13 await assertSameOrigin(); const u = await requireUser(); const { token } = await params; const d = db();14 const [m] = await d.select().from(listingMedia).where(and(eq(listingMedia.uploadToken, token), eq(listingMedia.status, "UPLOADING"), sql`${listingMedia.uploadedAt} > now() - interval '15 minutes'`)).limit(1);15 if (!m) return apiError(404, "not_found", "Jeton d'envoi invalide ou expiré");16 const l = await getListingFull(m.listingId); if (!l) return apiError(404, "not_found", "Annonce introuvable"); assertOwner(l, u.id, u.roles);17 const len = Number(req.headers.get("content-length") ?? 0); if (len > MAX_BYTES) return apiError(413, "too_large", "Image trop volumineuse (25 Mo max)");18 const buf = Buffer.from(await req.arrayBuffer()); if (buf.length > MAX_BYTES) return apiError(413, "too_large", "Image trop volumineuse (25 Mo max)");19 try {20 const r = await processUpload(buf, req.headers.get("content-type"));21 await d.update(listingMedia).set({ storageKey: r.key, mimeType: "image/webp", width: r.width, height: r.height, bytes: r.bytes, scanStatus: "CLEAN", status: "READY", uploadToken: null }).where(eq(listingMedia.id, m.id));22 await audit("media.upload", { actorId: u.id, objectType: "listing_media", objectId: m.id, meta: { bytes: buf.length, mime: r.mime } });23 return NextResponse.json({ ok: true, mediaId: m.id, url: `/api/media/${r.key}/800.webp`, width: r.width, height: r.height });24 } catch (e) {25 await d.update(listingMedia).set({ status: "DELETED", scanStatus: "REJECTED", uploadToken: null }).where(eq(listingMedia.id, m.id));26 if (e instanceof MediaError) { await audit("media.rejected", { actorId: u.id, objectId: m.id, meta: { code: e.code } }); return apiError(422, e.code, e.message); }27 throw e;28 }29 } catch (e) { return handle(e); }30}31